package com.scarecrow.olga.core.timer {
	
	import com.scarecrow.olga.core.IUpdateable;
	import com.scarecrow.olga.error.VerboseError;
	import com.scarecrow.olga.util.logging.Logger;
	import com.scarecrow.olga.util.logging.Profiler;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.utils.getTimer;
	
	/**
	 * This class provides a static acess to a time loop.
	 * Its main role is to send a synced update to all
	 * updateables registered.
	 * 
	 * It can also provide delayed calls to functions 
	 * running on the same synchronised loop.
	 * 
	 * @note This class must be statically initialized to
	 * have acces to the stage's ENTER_FRAME loop.
	 * 
	 * ...
	 * @author Santiago.J.Elizalde
	 */
	public class GameTimer {
		
		//updateables
		private static var updateList:Vector.<IUpdateable>;
		
		//custom callbacks
		private static var callbacksById:Object;
		private static var callbackList:Vector.<Callback>;
		private static var callbackCount:uint;
		private static var lastCallbackId:uint;
		
		//internal use
		private static var _stage:Stage
		private static var _startTime:uint;
		private static var _now:uint;
		private static var _frameSkip:int = 0;
		private static var _frameCount:int;
		
		//info
		private static var _currentFPS:Number;
		
		public static function init(stage:Stage):void {
			_stage = stage;
			_startTime = getTimer();
			
			updateList = new Vector.<IUpdateable>();
			callbacksById = new Object();
			callbackList = new Vector.<Callback>();
			
			_stage.addEventListener(Event.ENTER_FRAME, frameHandler);
			Logger.log("GameTimer initialized.", Logger.LOG_LEVEL_INFO);
		}
		
		/**
		 * Registers an IUpdateable instance to be updated along the game cycle.
		 * 
		 * @param	value the updateable object that needs to be registered
		 * @return	registration gameTime stamp
		 * @throws	Error if the updateable was already registered.
		 */
		public static function registerUpdateable(value:IUpdateable):uint {
			
			var idx:int = updateList.indexOf(value);
			if (idx > -1) throw new VerboseError("Unable to register updateable", "supplied updateable is already registered, duplicate entries are not allowed.");
			
			updateList.push(value);
			return _now;
		}
		
		/**
		 * Releases a previously registered IUpdateable instance.
		 * 
		 * @param	value the updateable object that needs to be released
		 * @throws	Error if the updateable was not registered.
		 */
		public static function releaseUpdateable(value:IUpdateable):void {
			
			var idx:int = updateList.indexOf(value);
			if (idx < 0) throw new VerboseError("Unable to release updateable", "supplied updateable does not exist in the registry.");
			
			updateList.splice(idx, 1);
		}
		
		/**
		 * Calls the supplied callback function after the specified delay.
		 * 
		 * @param	delay time until call in miliseconds.
		 * @param	callback the function that will be called.
		 * 
		 * @return	an id to cancel the call if needed.
		 */
		public static function callMeIn(delay:uint, callback:Function):uint {
			var cb:Callback = new Callback(++lastCallbackId, _now, delay, callback, false);
			callbacksById[cb.id] = cb;
			callbackList.push(cb);
			return lastCallbackId;
		}
		
		/**
		 * Calls the supplied callback function every a certain amount of time until
		 * cancelled.
		 * 
		 * @param	delay time between calls in miliseconds.
		 * @param	callback the function that will be called.
		 * 
		 * @return	an id to cancel the recurrent call.
		 */
		public static function callMeEvery(delay:uint, callback:Function):uint {
			
			var cb:Callback = new Callback(++lastCallbackId, _now, delay, callback, true);
			callbacksById[cb.id] = cb;
			callbackList.push(cb);
			return cb.id;
		}
		
		/**
		 * Cancels a previously registered call, either single time or repeating,
		 * by passing the ID assigned to the call (this ID is returned bu callmeIn()
		 * and callMeEvery() methods when registering a call).
		 * 
		 * @param	callId the id that was sent when the call was registered.
		 */
		public static function cancelCall(callId:uint):void {
			if (callId in callbacksById) {
				var cb:Callback = callbacksById[callId];
				cb.dispose();
				callbackList.splice(callbackList.indexOf(cb), 1);
				delete callbacksById[callId];
			}
			if (callbackList.length == 0) lastCallbackId = 0;
		}
		
		/**
		 * Current game time. The time elapsed since the gameTimer started ticking.
		 * 
		 * @note this value is NOT equivalent to calling flash's getTimer().
		 */
		public static function get gameTime():uint {
			return _now;
		}
		
		static public function get currentFPS():Number {
			return _currentFPS;
		}
		
		private static function frameHandler(e:Event):void {
			
			if (_frameSkip) {
				if (_frameCount == _frameSkip) {
					_frameCount = 0;
					return;
				}
				_frameCount++
			}
			var last:uint = _now;
			_now = getTimer() - _startTime;
			var elapsedTime:uint = _now - last;
			_currentFPS = 1000 / elapsedTime;
			
			for each (var call:Callback in callbackList) {
				// Add to callback time.
				call.acumTime += elapsedTime;
				
				if (call.acumTime >= call.delay) {
					// Time was met, call supplied listener.
					call.callback();
					if (!call.repeat) {
						// Remove the callback if it doesn't repeat.
						cancelCall(call.id);
					} else {
						// Reset callback timer if it repeats (keeping time difference since the delay was met).
						call.acumTime -= call.delay;
					}
				}
			}
			// Main update loop (done after the callback timers are executed)
			Profiler.start("generalUpdate");
			for each (var updateable:IUpdateable in updateList) {
				if (updateable.enabled) updateable.update(elapsedTime);
			}
			Profiler.end("generalUpdate");
		}
	}
}

internal class Callback {
	
	public var acumTime:uint;
	
	private var _callback:Function;
	private var _delay:uint;
	private var _id:uint;
	private var _stamp:uint;
	private var _repeat:Boolean;
	
	public function Callback(callId:uint, time:uint, callDelay:uint, callbackFunction:Function, willRepeat:Boolean) {
		_id = callId;
		_delay = callDelay;
		_callback = callbackFunction;
		_stamp = time;
	}
	
	public function get id():uint {
		return _id;
	}
	
	public function get delay():uint {
		return _delay;
	}
	
	public function get callback():Function {
		return _callback;
	}
	
	public function get timeStamp():uint {
		return _stamp;
	}
	
	public function get repeat():Boolean {
		return _repeat;
	}
	
	public function set timeStamp(value:uint):void {
		_stamp = value;
	}
	
	public function dispose():void {
		this._callback = null;
	}
}